
package uk.ac.cam.cl.oscar;

/* Yes, I realise we already have a convolution class
   However it's kinda hard-wired to be a gausian convolution
   filter, and at this point I wish not to **** around. here's another :)
   */

import java.awt.*;
import java.awt.image.*;
import java.awt.image.renderable.ParameterBlock;
import java.io.*;

import javax.media.jai.*;
import java.lang.*;
import javax.imageio.*;


import uk.ac.cam.cl.oscar.*;

public class ImageOperations {
	
	private static final double thumbnailSize = 150;
	
	public static BufferedImage load (File file)
	{
		try {
			return ImageIO.read (file);
		}
		catch (IOException e) {
			System.out.println ("Failed to load file " + file.getName());
			return null;
		}
	}
	
	public static BufferedImage load (String filename)
	{
		File file=new File(filename);
		return load (file);
	}
	
	public static void save (BufferedImage image, String filename) throws IOException
	{
		System.out.println ("This method will not work.. I do not know what to use as formatName");
		File out=new File(filename);
		ImageIO.write (image, "RGB", out);
	}
	
	public static BufferedImage createThumbnail (BufferedImage image)
	{
		double ratio = (double)image.getWidth() / (double)image.getHeight();
		double factor;
		
		if (ratio > 1.0) {
			// Scale down so width fills frame
			factor = thumbnailSize / (double)image.getWidth();
		}
		else {
			// Scale down so height fills frame
			factor = thumbnailSize / (double)image.getHeight();
		}
		
		return scale (image, factor);
	}
	
	public static BufferedImage perspectiveTransform (BufferedImage image, PerspectiveTransform pt)
	{
		ParameterBlock pb = new ParameterBlock();
        pb.addSource(image);
        pb.add(new WarpPerspective(pt));
       
		RenderedOp output = JAI.create("Warp", pb);

		return ImageConverter.toBuffered((PlanarImage)output);
	}
	
	/** Creates a new image of given size, with the input image centered or in the top left*/
	public static BufferedImage extendCanvas (BufferedImage image, int width, int height, boolean center) 
	{
		assert (width >= image.getWidth());
		assert (height >= image.getHeight());
		
		BufferedImage result = new BufferedImage (width, height, BufferedImage.TYPE_INT_RGB);
			
		int sx = center ? (width - image.getWidth())/2 : 0;
		int sy = center ? (height - image.getHeight())/2 : 0;
			
		for (int x = 0; x < image.getWidth(); x++) {
			for (int y = 0; y < image.getHeight(); y++) {
				result.setRGB (sx+x, sy+y, image.getRGB(x,y));	
			}
		}
		
		return result;
	}
	
	/** Creates a new image of given size, with the input image centered or in the top left*/
	public static BufferedImage shrinkCanvas (BufferedImage image, int width, int height, boolean center) 
	{
		assert (width <= image.getWidth());
		assert (height <= image.getHeight());
		
		BufferedImage result = new BufferedImage (width, height, BufferedImage.TYPE_INT_RGB);
			
		int sx = center ? (image.getWidth() - width)/2 : 0;
		int sy = center ? (image.getHeight() - height)/2 : 0;
			
		for (int x = 0; x < result.getWidth(); x++) {
			for (int y = 0; y < result.getHeight(); y++) {
				result.setRGB (x, y, image.getRGB(sx+x,sy+y));	
			}
		}
		
		return result;
	}
	
	/** Given an image and a rectangle inside it, copy the edge values outwards 
		The copying will take place from x1 outwards, etc. 
		
		This is done IN PLACE. No new image is created
		*/
	public static void copyBorders (BufferedImage image, int x1, int y1, int x2, int y2)
	{
		assert (x1 > 0 && x2 < image.getWidth() && y1 > 0 && y2 < image.getHeight());
		
		for (int x = 0; x < image.getWidth(); x++) {
			
			int sx = x < x1 ? x1 : x > x2 ? x2 : x;
			
			for (int y = 0; y < y1; y++) 
			{
				image.setRGB (x, y, image.getRGB(sx, y1));
			}
			
			for (int y = y2+1; y < image.getHeight(); y++) {
				image.setRGB (x, y, image.getRGB(sx, y2));
			}
		}
		
		for (int y = 0; y < image.getHeight(); y++) {
			
			int sy = y < y1 ? y1 : y > y2 ? y2 : y;
			
			for (int x = 0; x < x1; x++) 
			{
				image.setRGB (x, y, image.getRGB(x1, sy));
			}
			
			for (int x = x2+1; x < image.getWidth(); x++) {
				image.setRGB (x, y, image.getRGB(x2, sy));
			}
		
		}
		
	}
	
	/** Applys gaussian convolution. The image will get smaller, due to neighbourhood 
	    sampling (i.e. each pixel requires values from all sides)
	    */
	public static BufferedImage gaussian2D (BufferedImage image, int dimension, double sigma)
	{
		if (dimension >= image.getWidth() || dimension >= image.getHeight()) 
			throw new RuntimeException ("Cannot apply a blur filter larger than the image!");
			
		int oddDim = dimension | 1; // yay for bits and nibbles!
		float[] mtx = new float[oddDim*oddDim];
		
		double s2recip = 1.0 / (2.0 * sigma * sigma);
		double mul = s2recip / Math.PI;
		
		for (int i = -oddDim/2; i < oddDim/2; i++) {
			for (int j = -oddDim/2; j < oddDim/2; j++) {
				mtx[(oddDim/2+i) * oddDim + (oddDim/2+j)] = (float) (mul * Math.pow (Math.E, -(i*i+j*j)*s2recip));
			}
		}
		
		BufferedImage blurred = convolve2D (image, mtx, oddDim, oddDim);
		return blurred;
	}
	
	/* Scale an image by factor. factor=2 doubles the size */
	public static BufferedImage scale (BufferedImage image, double factor)
	{
		BufferedImage sized;
		if (factor > 1.0) {
			sized = extendCanvas (image, (int)Math.floor(image.getWidth() * factor), (int)Math.floor(image.getHeight() * factor), false);
		}
		else {
			sized = image;
		}
	
		ParameterBlock pb = new ParameterBlock();
        pb.addSource(sized);
        pb.add((float)factor); /* to scale by */
        pb.add((float)factor);
       
		RenderedOp output = JAI.create("scale", pb);

		return ImageConverter.toBuffered((PlanarImage)output);
	}
	
	/**
	 * Convolve with a 2D kernel
	   Clamps at edges
	 */
	public static BufferedImage convolve2D(BufferedImage image, float[] matrix, int mtxWidth, int mtxHeight) 
	{
		ParameterBlock pb;
		BufferedImage larger = extendCanvas (image, image.getWidth()+mtxWidth, image.getHeight()+mtxHeight, true);
		copyBorders (larger, mtxWidth/2, mtxHeight/2, image.getWidth()+mtxWidth/2-1, image.getHeight()+mtxHeight/2-1);
	
        // Normalise kernel
        float accum = 0;
        
        for (int i = 0; i < mtxWidth*mtxHeight; i++) {
        	accum += matrix[i];
        }
        if (accum > 0.01) {
			for (int i = 0; i < mtxWidth*mtxHeight; i++) {
				matrix[i] /= accum;
			}
        }
        
		// Create the kernel.
    	KernelJAI kernel = new KernelJAI(mtxWidth, mtxHeight, matrix);

		// Parameters
		pb = new ParameterBlock();
        pb.addSource(larger);
        pb.add(kernel);

     	// Create the convolve operation.
     	RenderedOp convolved = JAI.create("convolve", pb);
     	
     	
     	// Now crop
		BufferedImage cropped = shrinkCanvas (ImageConverter.toBuffered((PlanarImage)convolved), image.getWidth(), image.getHeight(), true);
     	
     	return cropped;
     	
	}
}
